北航co计算机组成P7debug分享和思考题

写在前面:
本人P7设计文档和P6大差不差,所以就放debug分享了,思考题附在了后面
P7 debug分享
一. 易错分析
有点啰嗦,按需阅读
首先,先让我们理清楚P7在P6的基础上增加了什么,以及哪些比较容易错。与此相对应的,我们在看P7的bug前请保证P6代码部分已经没有问题。
(以下实现都可能有其他情况)
CPU内部:
F级:
当D级指令是
eret时,F级下一个PC为EPC若采用不清空
eret后面的延迟槽的办法,F 级当前指令即为EPC当
Req == 1的时候,PC <= 32'H00004180易错点:
Req和reset和stall之间存在优先级请确保代码符合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22if(reset == 1)
begin
……
end
else
begin
if(Req == 1)
begin
PC <= 32'H00004180;
end
else
begin
if(stall == 1)
begin
……
end
else
begin
……
end
end
endReq == 1时除了W级以外(以CP0设在 M 级为例)全部流水级的PC均<=32'H00004180,正常情况下宏观PC应连续三个周期保持在32'H00004180
增加相应的检测异常的部分。此时可以检测出来的异常有
AdEL易错点:
AdEL错误发生后请把指令清空,只 需要把instr变成0即可- 在判断地址时,请和指导书要求保持一致,确保
<并没有写成<=等等
连线部分:
新增接口 连线部分 F_EPCCP0_EPCOutif_eretD_control_iferet(有其他实现)F_ReqCP0_Req其余部分实现差异可能较大,无论如何请保证连线正确。
FtoD流水线寄存器:增加对
Req == 1的相应部分易错点:
- 把PC改为
32'H00004180,而不是清零。 - 仍然是保证
Req和reset和stall之间的优先级 - 其余清零即可
- 把PC改为
当D级是
eret指令时清空延迟槽,保持PC不变(有其他实现方式)易错点:
不阻塞的时候才可以清空延迟槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15if(stall == 1)
begin
……
end
else
begin
if(if_eret == 1)
begin
……
end
else
begin
……
end
end
增加
BD、EXCcode的流水各部分的更改:
reset将PC改为32'H00003000,其余清零即可;Req将PC改为32'H00004180,其余清零即可;stall将所有值保持不变;清空延迟槽保持PC不变,其余清零即可。注意在阻塞注入空泡时
BD需正常流水(而非清零或保持当前值)连线部分
新增接口 连线部分 ReqCP0_Reqif_eret(如果需要)D_control_iferet(有其他实现)BD`D_control_Branch EXCcodeF_EXCcodeinstr 注意传异常处理之后清零的instr 其余部分实现差异可能较大,无论如何请保证连线正确。
D级:
D级需要信号能够反馈当前是否为
eret(给F) 和mtc0(为了暴力阻塞)原始控制信号部分增加
RegWrite = …… | mfc0(控制信号以分布式为例)易错点:
- 在判断指令
mfc0和mtc0时请注意它的特殊性(rs == mfc0)
- 在判断指令
增加相应的检测异常的部分。此时可以检测出来的异常有
Syscall和RI易错点:
- 检测到异常发生后请把指令清空,只 需要把IR变成0即可
- 虽然我们对
nop啥都不干,但是它是已知指令: (
**阻塞阻塞阻塞!**当D级是
eret时,如果E级和M级是mtc0,记得暴力阻塞一下。(cp0实现内部转发+E级到D级转发 /cp0实现内部转发+E级与D级阻塞也可以,但有可能会导致对拍不一致)给新增指令增加
Tuse和Tnew连线部分
好像没有特别需要注意的连线,提一句的就是把E级和M级的
if_mtc0传过来方便阻塞。
DtoE流水线寄存器:增加对指令是否是
mtc0的流水,用于暴力阻塞增加
BD、EXCcode的流水增加对
Req == 1的相应部分易错点:
- 仍然是保证
Req和reset和stall之间的优先级
- 仍然是保证
各部分的更改:
reset将PC改为32'H00003000,其余清零即可;Req将PC改为32'H00004180,其余清零即可;stall时PC改为当前在D级指令的新PC,BD改为当前在D级指令的新BD连线部分
EXCcode的流水,优先F级的异常,若F级没有异常再流水D级的异常(没有异常可以认为是0),如:EXCcode = (F_EXCcode != 0)? F_EXCcode : D_EXCcode;BD连接FtoD流水线寄存器输出的BD即可if_mtc0连接D_control_ifmtc0(有其他实现)instr注意发生异常后清零RegWrite注意发生异常后清零
E级:
在
ALU中判断是否溢出,采用指令集的方法即可1
2
3
4
5
6
7
8if(temp[32] != temp[31])
begin
overflow = 1;
end
else
begin
overflow = 0;
endMDU模块中增加Req。当Req为1的时候,乘除槽中原本的计算继续计算,但是不可以加入新的计算。(不可以取巧,比如室友想反正eret回到乘除指令都需要写入,那直接写入但先不开启延迟倒计时也没有区别,然而是异常处理程序部分会拿出LO和HI进行保存,这个时候就WA了)(本人在这里卡了好久,可能是乘除槽实现问题,如果对自己的实现不把握的话,可以单独对MDU模块进行仿真测试)增加相应的检测异常的部分。此时可以检测出来的异常有
AdEL、AdES、Ov(也可以在M级对这些异常进行检测,但是要注意异常的指令不可以对Bridge以及后续W级的REG产生任何影响)易错点:
检测到异常发生后可以选择把指令清空,防止对后续
DM和寄存器产生影响,只 需要把IR变成0即可(这样做其实有点风险)另一种实现思路是保留完整的异常指令一直到
M级,在Req = 1的时候禁访存(手动把byteen搞成 0)注意这里不能因为
GRF在CP0之后就不管GRF,如果不把受害指令清零的话错误操作照样会流水到W级对地址的判断较为繁琐,高频易错,细致细致,一定对着指导书的写(见过不少
AdEL、AdES判断错误的都是断章取义了)。确保符号正确,<并没有写成<=,||没有写成&&等等。确保地址数据正确,如计时器的Count寄存器是32'H0000_7F08和32'H0000_7F18(见证了室友写成32'H0000_7F28)
连线部分
把
MDU_Req连接CP0_Req
EtoM流水线寄存器:增加对指令是否是
mtc0的流水,用于暴力阻塞增加
BD、EXCcode的流水增加对
Req == 1的相应部分易错点:
- 仍然是保证
Req和reset之间的优先级
- 仍然是保证
各部分的更改:
reset将PC改为32'H00003000,其余清零即可;Req将PC改为32'H00004180,其余清零即可;连线部分
EXCcode的流水,优先D级的异常,若D级没有异常再流水E级的异常(没有异常可以认为是0),如:EXCcode = (D_EXCcode != 0)? D_EXCcode : E_EXCcode;BD连接DtoE流水线寄存器输出的BD即可if_mtc0连接DtoE流水线寄存器输出的if_mtc0即可(有其他实现)instr注意发生异常后清零RegWrite注意发生异常后清零如果
Regwrite不是流水的的话,在MtoW把IR清零也是一样的异常指令保留到
MtoW就可以放心清零了,因为W级不可能加异常的
M级:
本人将
CP0放在了M级,由于CP0的复杂性本人打算等会讲在
M_control中增加CP0OuttoReg、CP0en、iferet输出1
2
3assign CP0OutToReg = mfc0;
assign CP0en = mtc0;
assign iferet = eret;连线部分
异常的指令
byteen一定要保证是0。于是,现在我们基本保证了异常指令不会影响内存和寄存器。但是!**我们还需要保证外部中断到来的时候,当前指令也不会影响到内存和寄存器!**所以,请在
CP0_Req == 1时,保证byteen是0。
MtoW流水线寄存器和W级:- 没有什么特别需要修改的,只需要注意在
CP0_Req == 1时,保证RegWrite是0。
- 没有什么特别需要修改的,只需要注意在
CP0:CP0理论上可以说是各不相同,建议没把握可以对CP0模块进行单独仿真注意有异常中断的时候,
Req也不一定要更改,大致要求是1
2
3assign inter_req = (|(HWInt & SR_IM)) & SR_IE & (!SR_EXL);
assign exc_req = (EXcCodeIn != 5'd0) & (!SR_EXL);
assign Req = inter_req | exc_req;注意外部中断到来的时候,
mtc0也不应该修改CP0寄存器所以
mtc0也不应该修改CP0寄存器的条件是if((en == 1) && (Req == 0))外部中断优先级高于异常,此时注意
Cause_ExcCode存入的是0本人的实现出现了一个奇怪的bug,在这里分享一下。和室友探讨以后发现这是一个很奇怪的问题,至今也没有完全理解,欢迎大家找我探讨!
请看完再决定自己的代码在这部分有没有问题,因为。。
以下三个版本可能看着没有区别。所以我决定把我的仿真结果跟在后面,以下是tb文件(数据全乱编的,可能不符合CPU逻辑,但是我觉得对CP0测试应该没有问题)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69module cp0_test;
// Inputs
reg clk;
reg reset;
reg en;
reg [4:0] CP0Add;
reg [31:0] CP0In;
reg [31:0] VPC;
reg BDIn;
reg [4:0] EXcCodeIn;
reg [5:0] HWInt;
reg EXLClr;
// Outputs
wire [31:0] CP0Out;
wire [31:0] EPCOut;
wire Req;
// Instantiate the Unit Under Test (UUT)
CP0 uut (
.clk(clk),
.reset(reset),
.en(en),
.CP0Add(CP0Add),
.CP0In(CP0In),
.CP0Out(CP0Out),
.VPC(VPC),
.BDIn(BDIn),
.EXcCodeIn(EXcCodeIn),
.HWInt(HWInt),
.EXLClr(EXLClr),
.EPCOut(EPCOut),
.Req(Req)
);
initial begin
// Initialize Inputs
clk = 1;
reset = 1;
en = 0;
CP0Add = 0;
CP0In = 0;
VPC = 0;
BDIn = 0;
EXcCodeIn = 0;
HWInt = 0;
EXLClr = 0;
#5;
reset = 0;
#5;
VPC = 10;
BDIn = 1;
EXcCodeIn = 4;
HWInt = 0;
EXLClr = 0;
#10;
VPC = 14;
BDIn = 0;
EXcCodeIn = 0;
HWInt = 0;
EXLClr = 0;
end
always #5 clk = ~clk;
endmodule我的初版是:
1
2
3
4
5
6
7
8if(Req == 1)
begin
`SR_EXL <= 1'b1;
`Cause_BD <= BDIn;
`Cause_ExcCode <= (inter_req)? 5'd0 : EXcCodeIn;
`EPC <= (BDIn == 1)? (VPC - 32'd4) : VPC;
end
`Cause_IP <= HWInt;但是我对
CP0模块进行单独仿真时发现,这个时候EPC已经是错误指令的下一个周期的对应值了如下图所示:

图1 p. s. 这个明显应该是错误的,但是这个错误当时并没有被COT检测出来()
我的初步理解是,我的
Req并没有在时钟上升沿到来的时候及时改变,即10ns时钟上升沿到来时,Req没有及时变为1,所以if(Req == 1)这一块代码没有在这一周期运行;在20ns时钟上升沿到来时,Req没有及时变为0,所以if(Req == 1)这一块代码在这一周期运行了于是改成了
1
2
3
4
5
6
7
8if(((|(HWInt & `SR_IM)) & `SR_IE & (!`SR_EXL)) || ((EXcCodeIn != 5'd0) & (!`SR_EXL)))
begin
`SR_EXL <= 1'b1;
`Cause_BD <= BDIn;
`Cause_ExcCode <= ((|(HWInt & `SR_IM)) & `SR_IE & (!`SR_EXL))? 5'd0 : EXcCodeIn;
`EPC <= (BDIn == 1)? (VPC - 32'd4) : VPC;
end
`Cause_IP <= HWInt;对照一下,你会发现,其实我就是把
Req改成了组成他的部分。但是仿真结果出现了巨大改变:
图2 诶EPC对了!
此时整体结果应该是没有任何问题了
但是仔细观察,你会发现,诶?我
Req怎么没变过!?我的理解是,
Req改变条件中有SR_EXL,而运行if(((|(HWInt &SR_IM)) &SR_IE & (!SR_EXL)) || ((EXcCodeIn != 5’d0) & (!SR_EXL)))模块的时候快速更改了SR_EXL,于是出现了Req昙花一现式改变的情况所以看似没变,只是
Req迅速改变为1又迅速改变为0,仿真没有体现出来为了能够体现出来
Req的改变,于是有了第三版:1
2
3
4
5
6
7
8
9
10
11if(Req == 1)
begin
`SR_EXL <= 1'b1;
end
if(((|(HWInt & `SR_IM)) & `SR_IE & (!`SR_EXL)) || ((EXcCodeIn != 5'd0) & (!`SR_EXL)))
begin
`Cause_BD <= BDIn;
`Cause_ExcCode <= ((|(HWInt & `SR_IM)) & `SR_IE & (!`SR_EXL))? 5'd0 : EXcCodeIn;
`EPC <= (BDIn == 1)? (VPC - 32'd4) : VPC;
end
`Cause_IP <= HWInt;然后就没有问题了:

这样看起来实现得很丑陋,但是可行,但是丑陋。
当你看完这段,你去看自己的代码,大部分人可能会想,我不会是错的吧(本人室友就这想了)
然而是!然而是!
她与我的第一版实现几乎一致,但是仿真结果如第二版所示,也就是没有问题!
以下是她这部分的代码:
1
2
3
4
5
6
7`CAUSE_IP <= HwInt;
if (Req) begin
`CAUSE_BD <= BD;
`SR_EXL <= 1'b1;
EPC <= (BD) ? (Vpc - 4) : Vpc;
`CAUSE_EXCCODE <= (interruptJudge) ? 5'b00 : excCode;
end仿真结果是:

EPC正常改变了!
仔细比较我们CP0的区别,只有:
我在
CP0开了数组,实现所有寄存器1
reg [31:0] mem [0:31];
而她只实现了部分寄存器
1
2
3
4reg [31:0] SR;
reg [31:0] CAUSE;
reg [31:0] EPC;
reg [31:0] PRID;但感觉应该不是这个区别导致的,并没有理解为什么: (
有想法的话欢迎探讨!
Req只会产生一个周期,在中断处理程序过程中一直是1的是SR_EXL,直到eret到达M级使得(EXLClr == 1),SR_EXL <= 1'b0;连线部分
接口 连线 CP0_enM_control_CP0enCP0AddIR[rd]CP0InM级转发后的 GRF[rt]CP0_VPCEtoM_PCCP0_BDInEtoM_BDCP0_EXcCodeInEtoM_EXCcodeCP0_HWIntCPU_HWIntCP0_EXLClrM_control_iferet
CPU和外部的连线:
额。。其实感觉只要内部实现了,与外部连线传输的数据就不怎么需要更改。还是强调一下异常中断时,当前指令也不应该影响到内存和寄存器,保证
byteen == 0和RegWrite == 0。另外注意传输CP0所在一级的PC作为宏观PC。
Bridge:
同样也是各不相同的实现方式,没把握可以对该模块进行单独仿真,现说明一些易错:
- 地址地址地址。
| 地址 | 数值 |
|---|---|
| DM | (A >= 32'H0000_0000 && A <= 32'H0000_2FFF) |
| T0 | (A >= 32'H0000_7F00 && A <= 32'H0000_7F0B) |
| T1 | (A >= 32'H0000_7F10 && A <= 32'H0000_7F1B) |
| Int | (A >= 32'H0000_7F20 && A <= 32'H0000_7F23) |
保证Timer0 输出的中断信号接入 HWInt[0] (最低中断位),Timer1 输出的中断信号接入 HWInt[1],来自中断发生器的中断信号接入 HWInt[2]。(小心接错位)
1
assign HWInt = {3'b000, interrupt, Timer1_IRQ, Timer0_IRQ};
Timer:
课程组已经给了该板块了,但是但是不仔细阅读会出错。传入的地址是30位input [31:2] Addr !!!
二. 对拍!
本人不会搭建评测机,但是我们有伟大的 COT评测机
感谢学长感谢学长!(我也用过cokiller,我们的输出和它mars对拍会多很多行,而且好像无法和同学对拍,所以就不过多叙述了)
但是COT对拍会有一点差异性:分为和mars对拍、和同学对拍进行讲述吧!
和mars对拍
mars在遇到
sb和lb指令读取32'H0000_2FFF的时候会标记为异常指令,大致情形如下:1
2Mars: "@00004180: $26 <= 00000414"
P7: "@0000388c: *00002ffc <= 69000000"mars在读取计时器的
Count寄存器时,会和我们有所不同,大致情形如下:1
2
3Mips Code: "lw $15, 61($20)" in line 285
Mars: "@00003400: $15 <= ffffffff"
P7: "@00003400: $15 <= 00000000"仔细debug然后会发现lw读取的是
Count寄存器
和同学对拍
由于每个人实现的不一样,会导致对拍的差异性:
eret实现的不一样
清空延迟槽
通过一个多路选择器,当D时eret时直接将F的
PC改为EPC
每一次中断异常,后者会比前者少一个周期,这样会导致到后期
Timer产生的中断完全对不上阻塞转发实现的不一样。
这个就比较多样性了,如
lui的Tnew其实可以是1,会导致周期差异。同样面对mtc0和eret暴力阻塞还是转发,也会导致周期差异(甚至你乘除槽延长的时间是不是5/10周期也会有影响)
对拍具体差异如下:
1
2
3
4verilog@P7_1 execute: "mfc0 $k0, $13" in line 1244
verilog@P7_1: "@00004180: $26 <= 00000410"
verilog@P7_2 execute: "mfc0 $k0, $13" in line 1244
verilog@P7_2: "@00004180: $26 <= 00000010"此时具体去看会发现是
Cause_IP的不同,再具体去查看就会发现此时timer_IRQ不一样,然后就会发现其实运行时间有很大差异。cot不足
经过第一次强测,我们发现cot评测机没有BD位输出错误进行测试。具体是,没有对jr指令后延迟槽指令是否出现中断异常(涉及到BD位输出错误)进行测试。而本人之前也出现了cot评测全过但是弱测没过的情况: ( 。不过真的很感谢学长的cot评测机了。
p. s. 据我所知,上一届强测没有检测
Timer有关错误,所以拿到的上一届强测过了的代码不一定完全正确哦~
三. debug思路分享
当我们发现cot评测机wa之后,我们可以选择吧code.txt放到我们的P7文件夹中进行仿真。code.txt一般比较长,记事本左上角的查找功能可以帮助我们找到我们错的那一行,然后观察上下文。mars程序也可以送给ai让他给我们标上PC。如果PC在4180那一段,基本是找不到bug的,我们需要知道异常中断之前发生了什么,可以往回找PC到3xxx,找对应寄存器的错误,然后记住PC,在仿真里面找到他,如果是寄存器,就一层一层展开路径上的所有模块看哪个结果不符合预期(可以把每一级的PC都拎出来方便观察)。如果是内存的话,就要算一下理想中是读那一段的内存,然后再一层一层展开。然后就是对着错误模块干瞪眼了,当然对错误模块进行单独仿真也不错。
四. 最后
感谢我伟大善良智慧的室友大人给予本篇文章的众多意见和想法!
有任何想法意见欢迎讨论。有任何错误欢迎纠正!
祝大家co顺利!
思考题点击展开,不保证正确性,建议自己思考
思考题
1、请查阅相关资料,说明鼠标和键盘的输入信号是如何被 CPU 知晓的?
鼠标和键盘作为外设,其输入信号通常通过中断机制被 CPU 知晓。具体过程如下:
硬件接口:鼠标和键盘通过 USB、PS/2 等接口连接到计算机系统。
中断信号:当用户按下按键或移动鼠标时,外设控制器会向 CPU 发送一个外部中断信号。
中断控制器:该信号通常先由中断控制器(如 8259A 或 APIC)管理,设置相应的中断标志。
CPU 响应:CPU 在每条指令执行结束时检查是否有中断请求。若有,则暂停当前程序,保存现场,跳转到中断服务程序入口地址。
中断服务程序:读取外设数据寄存器,获取输入内容,进行处理。
2、请思考为什么我们的 CPU 处理中断异常必须是已经指定好的地址?如果你的 CPU 支持用户自定义入口地址,即处理中断异常的程序由用户提供,其还能提供我们所希望的功能吗?如果可以,请说明这样可能会出现什么问题?否则举例说明。(假设用户提供的中断处理程序合法)
CPU 处理中断异常必须是固定入口地址的原因在于:
1.硬件设计简化:固定入口地址使得 CPU 硬件实现简单,无需额外寄存器存储用户提供的入口地址。
2.系统安全与控制:如果允许用户自定义入口地址,可能出现以下问题:
*恶意代码执行:用户可能将入口地址指向恶意代码,破坏系统。
*系统一致性破坏:用户程序可能未正确处理异常,导致系统状态不一致。
*优先级与嵌套问题:用户程序可能未正确处理中断嵌套或优先级,导致系统死锁或响应异常。
若支持用户自定义入口地址,虽能提供灵活性,但会引入安全性和可靠性风险,需额外的硬件保护机制(如特权模式、内存保护)来限制。
3、为何与外设通信需要 Bridge?
与外设通信需要系统桥(Bridge)的原因包括:
1.接口统一化:外设种类繁多,接口各异。系统桥提供统一的读写接口(如内存映射 I/O),使 CPU 可以像访问内存一样访问外设。
2.简化 CPU 设计:CPU 无需为每种外设设计专用指令或接口,符合“高内聚、低耦合”设计原则。
3.地址映射与解码:系统桥负责将 CPU 发出的地址映射到相应的外设,并处理读写时序与协议转换。
4.扩展性与模块化:新增外设时只需接入系统桥,无需修改 CPU 设计。
4、请阅读官方提供的定时器源代码,阐述两种中断模式的异同,并分别针对每一种模式绘制状态移图。
模式0下的中断信号将持续有效,直至控制寄存器中的中断屏蔽位被设置为0。
不同于模式0,模式1下计数器每次计数循环中只产生一周期的中断信号。
5、倘若中断信号流入的时候,在检测宏观 PC 的一级如果是一条空泡(你的 CPU 该级所有信息均为空)指令,此时会发生什么问题?在此例基础上请思考:在 P7 中,清空流水线产生的空泡指令应该保留原指令的哪些信息?
(一) 如果中断信号到达时,宏观 PC 所在的流水级是一条空泡指令(NOP,所有信息为空),会发生:
无法确定异常入口:空泡指令没有有效的 PC 值,导致无法正确写入 EPC,也无法确定异常返回地址。
中断响应延迟:CPU 可能无法响应中断异常。
(二) 在 P7 中,清空流水线产生的空泡指令应保留以下信息:
PC 值:用于异常返回地址计算。
指令类型标记:标记是否为有效指令或异常指令。
流水线状态标记:如是否处于延迟槽。
6、为什么 jalr 指令为什么不能写成 jalr $31, $31?
jalr指令的功能是跳转到对应寄存器中的地址,同时把PC+4(有延迟槽则PC+8)的值写入对应寄存器中。如果后面两个寄存器都是相同的,则先跳转再写入还是先写入再跳转就会产生两种不一样的结果。因此为了避免这种未知情况的发生,不能这样写。
